CDKを使って、一つのLambda関数でAPI設計してみた
複数のAPIを作る際のLambda関数の構成
今回の一つのLambda関数だけを用いた構成だと、Lambda関数が一つであるためデプロイの時間を短くすることができるというメリットがあります。一方で、複数のAPIを作るために一つのLambda関数内で、httpメソッド(GET,POST,PUT,DELETE)ごとに条件分岐を行い、さらにURLごとに条件分岐する必要があります。またURLの条件分岐では正規表現が出てきます。他の人が見た時に、ぱっと見ではわかりにくいコードになってしまうというデメリットがあります。
それに対して、APIの数だけLambda関数を作成する構成だと、何度も条件分岐が出てくることは無く、正規表現も出てこないため分かりやすいコードに出来るというメリットがあります。一方で、Lambda関数が多いとその分デプロイに時間がかかるというデメリットがあります。
両方の構成のメリットとデメリットを比較すると多くの場合、APIの数だけLambda関数を作成する構成の方が良いのではと思います。しかし、今回はお勉強のために一つのLambda関数だけを用いた構成を試してみることにしました。
ググって見つけたものですが、以下のリポジトリでは、APIの数だけLambda関数を作成しています。また自分が今回行ったのと同じように、DynamoDBに対してCRUD操作を行うAPIとなっています。
AWSの構成
CDK v1からv2へのマイグレーション
REST API
- データ一覧取得
- IDが001のデータを取得
- データ作成
- IDが001のデータを更新
- IDが001のデータを削除
Lambda関数
import { APIGatewayProxyHandlerV2 } from "aws-lambda";
import {
DynamoDBClient,
ScanCommand,
ScanCommandInput,
...
} from "@aws-sdk/client-dynamodb";
const dynamoDBClient = new DynamoDBClient({});
const TABLE_NAME = "items";
export const handler: APIGatewayProxyHandlerV2 = async (
event: any = {}
): Promise => {
const requestedMethod = event.httpMethod;
const requestedPath = event.path;
const requestedPathParameters = event.pathParameters;
switch (requestedMethod) {
case "GET": {
return getReceiver(
requestedPath,
requestedPathParameters ? requestedPathParameters.id : ""
);
}
case "POST": {
return postReceiver(...)
}
...
default: {
return {
statusCode: 400,
body: `Error: You are missing the path parameter id`,
};
}
}
};
async function getReceiver(
requestedPath: string,
requestedItemId: string | null
) {
switch (true) {
// path=/api/v1/items/
case /\/api\/v1\/items\/?$/.test(requestedPath): {
const params: ScanCommandInput = {
TableName: TABLE_NAME,
ProjectionExpression: "itemId, itemName",
};
try {
const response = await dynamoDBClient.send(new ScanCommand(params));
return { statusCode: 200, body: JSON.stringify(response) };
} catch (dbError) {
return { statusCode: 500, body: JSON.stringify(dbError) };
}
}
// path=/api/v1/items/000/
case /\/api\/v1\/items\/[0-9]{3}\/?$/.test(requestedPath): {
...
}
default: {
return {
statusCode: 400,
body: `Error: You are missing the path parameter id`,
};
}
}
}
...
詰まった箇所
この構成にしたから、詰まったというわけではありませんが、DynamoDBにデータを書き込む際にエラーが発生しました。詰まった箇所は、Lambdaが受け取ったPOSTリクエストのbody部に含まれるデータをDynamoDBに挿入する、という部分です。デプロイ後にAPIを呼び出してみると500番が返ってきました。Lambdaの方でエラーログをみると以下のように書かれていました。
TypeError: Cannot read property '0' of undefined
最初DynamoDBの設定が間違っているのかと思いましたが、DynamoDBに挿入しようとしている文字列がundefinedになっていることに気づきました。
// postリクエストで受け取るbodyの型
interface WriteBody {
itemId: string;
itemName: string;
}
// Lambdaのhandler関数
export const handler: APIGatewayProxyHandlerV2 = async (
event: any = {}
): Promise => {
...
case "POST": {
return postReceiver(requestedPath, event.body);
}
...
}
postReceiver関数の引数にevent.bodyが指定されていますが、event.body.itemIdとしてもundefinedが返ってきます。そのため上記のコードの13行目は、
return postReceiver(requestedPath, JSON.parse(event.body));
このように変更して、postReceiver関数にはevent.bodyをjsonにパースしてから渡すことで値を参照できるようになりました。
感想
CDKをしっかり触ったことも、APIを自分で作った経験も、ほとんど無かったのでとても勉強になりました。クラウドのデータベースに対してCRUD操作を行うAPIを一人で作れるなんて、自分で出来ることが増えてきて嬉しいです。今回はCDKを使ってAPIを作りましたが、Express.jsのようなフレームワークを使ってAPIを使ってみてもいいのかなと思いました。時間があれば今度はExpressも勉強してみようと思います。Lambda関数を使ったAPI設計が主題だったので、あまり触れませんでしたが、AWS SDK(v3)を使ってDynamoDBに対して操作を行うのも少し難しかったです。その辺りに関しては以下のブログが参考になると思います。